package com.aplink.generic.google.maps.clustering.algo;

import java.util.Collection;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import android.support.v4.util.LruCache;

import com.aplink.generic.google.maps.clustering.Cluster;
import com.aplink.generic.google.maps.clustering.ClusterItem;

/**
 * Optimistically fetch clusters for adjacent zoom levels, caching them as
 * necessary.
 */
public class PreCachingAlgorithmDecorator<T extends ClusterItem> implements
        Algorithm<T> {
	private class PrecacheRunnable implements Runnable {
		private final int mZoom;

		public PrecacheRunnable(final int zoom) {
			mZoom = zoom;
		}

		@Override
		public void run() {
			try {
				// Wait between 500 - 1000 ms.
				Thread.sleep((long) ((Math.random() * 500) + 500));
			} catch (final InterruptedException e) {
				// ignore. keep going.
			}
			getClustersInternal(mZoom);
		}
	}

	private final Algorithm<T> mAlgorithm;
	// TODO: evaluate maxSize parameter for LruCache.
	private final LruCache<Integer, Set<? extends Cluster<T>>> mCache = new LruCache<Integer, Set<? extends Cluster<T>>>(
	        5);

	private final ReadWriteLock mCacheLock = new ReentrantReadWriteLock();

	public PreCachingAlgorithmDecorator(final Algorithm<T> algorithm) {
		mAlgorithm = algorithm;
	}

	@Override
	public void addItem(final T item) {
		mAlgorithm.addItem(item);
		clearCache();
	}

	@Override
	public void addItems(final Collection<T> items) {
		mAlgorithm.addItems(items);
		clearCache();
	}

	private void clearCache() {
		mCache.evictAll();
	}

	@Override
	public void clearItems() {
		mAlgorithm.clearItems();
		clearCache();
	}

	@Override
	public Set<? extends Cluster<T>> getClusters(final double zoom) {
		final int discreteZoom = (int) zoom;
		final Set<? extends Cluster<T>> results = getClustersInternal(discreteZoom);
		// TODO: Check if requests are already in-flight.
		if (mCache.get(discreteZoom + 1) == null) {
			new Thread(new PrecacheRunnable(discreteZoom + 1)).start();
		}
		if (mCache.get(discreteZoom - 1) == null) {
			new Thread(new PrecacheRunnable(discreteZoom - 1)).start();
		}
		return results;
	}

	private Set<? extends Cluster<T>> getClustersInternal(final int discreteZoom) {
		Set<? extends Cluster<T>> results;
		mCacheLock.readLock().lock();
		results = mCache.get(discreteZoom);
		mCacheLock.readLock().unlock();

		if (results == null) {
			mCacheLock.writeLock().lock();
			results = mCache.get(discreteZoom);
			if (results == null) {
				results = mAlgorithm.getClusters(discreteZoom);
				mCache.put(discreteZoom, results);
			}
			mCacheLock.writeLock().unlock();
		}
		return results;
	}

	@Override
	public Collection<T> getItems() {
		return mAlgorithm.getItems();
	}

	@Override
	public void removeItem(final T item) {
		mAlgorithm.removeItem(item);
		clearCache();
	}
}
